﻿using Google.Protobuf;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

class PendingAckInfo
{
    public float m_Time;
    public uint m_SequenceID;
    public byte m_ResendCount;
    public uint m_UserID;
    public IMessage m_Message;
}

class PendingAckManager
#if !SERVER
    : TSingleton<PendingAckManager>
#endif
{
    readonly int MAX_PENDING_ACKS = 32;
    readonly int MAX_RESEND_COUNT = 3;
    List<PendingAckInfo> m_PendingAcks = new List<PendingAckInfo>();
    readonly float RECOVERY_TIME_OUT = 5;
    Action<uint, IMessage> m_OnResend;

    public void RegisterResendCallback(Action<uint, IMessage> action)
    {
        m_OnResend = action;
    }

    public PendingAckInfo GetAckInfo(uint userId, IMessage message)
    {
        return m_PendingAcks.Find((temp) => temp.m_UserID == userId && temp.m_Message == message);
    }

    public void AddInfo(uint userId, uint seq, IMessage msg)
    {
        if (!MessageManager.Instance.IsReliable(msg))
        {
            return;
        }

        lock (m_PendingAcks)
        {
            var info = m_PendingAcks.Find((temp) => temp.m_SequenceID == seq);
            if (info == null)
            {
                m_PendingAcks.Add(new PendingAckInfo
                {
                    m_Time = CommonUtil.GetCurElapsedTime(),
                    m_Message = msg,
                    m_SequenceID = seq,
                    m_ResendCount = 0,
                    m_UserID = userId,
                });
                LogManager.Log("add seqId=" + seq + ", message=" + msg.GetType());
            }
            else
            {
                info.m_Time = CommonUtil.GetCurElapsedTime();
                info.m_ResendCount++;
                if (info.m_ResendCount >= MAX_RESEND_COUNT)
                {
                    m_PendingAcks.Remove(info);
                }
                LogManager.Log("add resend seqId=" + seq + ", message=" + msg.GetType());
            }
            // TODO 暂时先不要循环比，假定最大seq不超过int的最大值
            m_PendingAcks.Sort((x, y) => (int)x.m_SequenceID - (int)y.m_SequenceID);
        }
    }

    public void RemoveInfo(uint seq)
    {
        lock (m_PendingAcks)
        {
            LogManager.Log("try remove seqId=" + seq);
            if (m_PendingAcks.Count == 0)
            {
                return;
            }
            var dest = m_PendingAcks.Find((temp) => temp.m_SequenceID == seq);
            if (dest != null)
            {
                LogManager.Log("seq=" + seq + ", message=" + dest.m_Message.GetType());
                m_PendingAcks.Remove(dest);
            }
        }
    }

    public void RecvAck(uint seqId, uint ackBits)
    {
        LogManager.Log("recv ack seqId=" + seqId);
        if (m_PendingAcks.Count == 0)
        {
            return;
        }

        uint seq = seqId;
        RemoveInfo(seq);
        bool needCheck = false;
        for (int i = 0; i < MAX_PENDING_ACKS; i++)
        {
            needCheck = (ackBits & 1 << i) >> i == 1;
            if (!needCheck)
            {
                continue;
            }
            var id = (seqId - i - 1);
            if (id < 0)
            {
                // TODO
                continue;
            }
            seq = (uint)id;
            RemoveInfo(seq);
        }
    }

    public void Resend(uint userId, IMessage msg)
    {
        if (m_OnResend != null)
        {
            m_OnResend(userId, msg);
        }
    }

    public void Tick()
    {
        lock (m_PendingAcks)
        {
            //LogManager.Error("pending count=" + m_PendingAcks.Count);
            if (m_PendingAcks.Count == 0)
            {
                return;
            }
            var latestSeqId = m_PendingAcks[m_PendingAcks.Count - 1].m_SequenceID;
            var now = CommonUtil.GetCurElapsedTime();
            for (int i = m_PendingAcks.Count - 1; i >= 0; i--)
            {
                var item = m_PendingAcks[i];
                if (latestSeqId - item.m_SequenceID > MAX_PENDING_ACKS
                    || now - item.m_Time >= RECOVERY_TIME_OUT)
                {
                    // TODO
                    item.m_Time = CommonUtil.GetCurElapsedTime();
                    Resend(item.m_UserID, item.m_Message);
                }
            }
        }
    }
}
